{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Предсказание объема оборота в магазинах компании Rossman\n",
    "\n",
    "<img src='../../img/rossman_logo.png'>\n",
    "\n",
    "**План исследования**\n",
    " - Описание набора данных и признаков\n",
    " - Первичный анализ признаков\n",
    " - Первичный визуальный анализ признаков\n",
    " - Закономерности, \"инсайты\", особенности данных\n",
    " - Предобработка данных\n",
    " - Создание новых признаков и описание этого процесса\n",
    " - Кросс-валидация, подбор параметров\n",
    " - Построение кривых валидации и обучения \n",
    " - Прогноз для тестовой или отложенной выборки\n",
    " - Оценка модели с описанием выбранной метрики\n",
    " - Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[Данные](https://www.kaggle.com/c/rossmann-store-sales) представляют собой информацию об объеме оборота в 1115 магазинах компании Rossman в Германии в зависимости от предоставленных признаков. Ассортимент представлен от бытовой химии и детских принадлежностей до косметики. Раньше менеджеры магазинов сами предсказывали число продаж основываясь на собственном мнении. Вследствие чего разброс предсказанных значений был высоким.\n",
    "\n",
    "Решается задача регрессии. Она состоит в том, чтобы наиболее точно предсказать число продаж определнного магазина в определенный день. Предсказание меньшего числа, ровно как и большего может привести к убыткам.\n",
    "\n",
    "Данные представлены во временном промежутке от 1 января 2013 года до 31 июля 2015 года. Также стоит отметить, что есть временной признак, значимость которого еще следует установить.\n",
    "\n",
    "Источником данных служит соревнование на Kaggle."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Переменные"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Общая информация о магазине"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Id - ID, который идентифицирует магазин и дату.\n",
    "2. Store - ID, который присвоен каждому магазину.\n",
    "3. Customers - число клиентов. (в тесте отстутствует)\n",
    "4. Open - некоторые магазины могли быть закрыты на ремнот. Идентифицирует открыт ли сейчас магазин: 0 = закрыт, 1 = открыт.\n",
    "5. StoreType - магазины представлены четырьмя видами: a, b, c, d. Дополнительной информации не дано.\n",
    "6. Assortment - уровень ассортимента: a = basic, b = extra, c = extended."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Информация о выходных днях"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. StateHoliday - является ли представленный день выходным. Обычно магазины в выходные не работают. Также все школы закрыты во время любых выходных. a = общий выходной, b = Пасха, c = Рождество, 0 = Нет\n",
    "2. SchoolHoliday - отражает было ли наблюдение подвержено наличию школьных выходных (0, 1). Очевидно, что наличие влияния автоматичесеки приводит к наличию самих школьных выходных."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Наличие конкурентов поблизости"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. CompetitionDistance - расстояние до ближайшего конкурента.\n",
    "2. CompetitionOpenSince[Month/Year] - дата открытия конкурента. Видимо, NaN значит, что конкурент был открыт позже представленного магазина."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Промоакции индивидуальные для каждого магазина и, в некторых случаях, дня"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Promo - проводит ли магазин акцию.\n",
    "2. Promo2 - некторые магазины участвуют в прродлжительной и периодичной акции: 0 = не участвует, 1 = участвует\n",
    "3. Promo2Since[Year/Week] - когда магазин начал принимать участие в акции Promo2.\n",
    "4. PromoInterval - задает интервалы перезапуска акции Promo2. Например, \"Feb,May,Aug,Nov\" значи, что акция перезапускается в Феврале, Мае, Августе и Ноябре каждый год."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Временная шкала"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Date: дата наблюдения\n",
    "2. DayOfWeek: день недели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Целевая переменная"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Sales - оборот данного магазина в данный день. Это значение будет предсказываться."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также признаки можно разделить на две группы так же, как они разделены на два файла train.csv и store.csv:\n",
    "1. Зависят от времени: 'DayOfWeek', 'Date', 'Sales', 'Customers', 'Open', 'Promo', 'StateHoliday', 'SchoolHoliday'.\n",
    "2. Зависят только от локации: 'StoreType', 'Assortment', 'CompetitionDistance', 'CompetitionOpenSinceMonth', 'CompetitionOpenSinceYear', 'Promo2', 'Promo2SinceWeek', 'Promo2SinceYear', 'PromoInterval'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "sns.set_style('darkgrid')\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.linear_model import LinearRegression, SGDRegressor, Lasso, LassoCV, RidgeCV\n",
    "from sklearn.model_selection import train_test_split, TimeSeriesSplit\n",
    "from sklearn.metrics import mean_squared_error\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.model_selection import cross_val_score, learning_curve, validation_curve\n",
    "from scipy.stats import normaltest, skewtest, skew\n",
    "from scipy.sparse import csr_matrix, hstack\n",
    "%matplotlib inline\n",
    "pd.set_option('max_columns', 100)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 2. Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Считаем данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train = pd.read_csv('data/train.csv')\n",
    "train.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test = pd.read_csv('data/test.csv')\n",
    "test.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stores = pd.read_csv('data/store.csv')\n",
    "stores.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(train.info(), \"\\n\")\n",
    "print(test.info(), \"\\n\")\n",
    "print(stores.info())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можно заметить, что признак Open в тестовом датасете типа float."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для дальнейшего удобства визуализации сконвертируем некоторые признаки из типа object в тип int. Данные числа не будут иметь ничего общего со значениями этих признаков. В дальнейшем к ним нужно будет применить более осмысленную предобработку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "letters_to_ints = {'a': 1, 'b': 2, 'c': 3, 'd': 4}\n",
    "stores['StoreType'] = stores['StoreType'].map(letters_to_ints)\n",
    "stores['Assortment'] = stores['Assortment'].map(letters_to_ints)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stores.hist(figsize=(10, 10));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Взглянем подробнее на наиболее интресные графики."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.countplot(stores['CompetitionOpenSinceMonth']);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(15, 5))\n",
    "sns.countplot(stores['Promo2SinceWeek']);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.countplot(stores['Promo2SinceYear']);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видны пики и падения в определенное время года. Возможно есть периодичность, но пока этого заключить нельзя. Может они как-то связаны с целевой перменной? Это подробнее рассмторим позже, когда объединим таблицы."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на зависимость между ассортиментом и типом магазина."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(stores['Assortment'], stores['StoreType'], margins=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что из значения Assortment 'extra' следует второй тип магазина. Также стандартный ассортимент чаще встречается в магазинах первого типа, а расширенный в магазинах четвертого типа."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Напоследок посмотрим на признак PromoInterval. Он чистый и заполнен по образцу. С ним будет удобно работать."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stores['PromoInterval'].unique()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.countplot(stores['PromoInterval']);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Чаще всего магазины пользовались схемой \"Январь, Апрель, Июль, Октябрь\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Объединим таблицы, так как предсказывать нужно для наблюдений. Каждому наблюдению припишем признаки соответствующего магазина. Также отсортируем наблюдения по времени."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "stores_dict = {}\n",
    "stores_dict['StoreType'] = dict(zip(stores.Store, stores.StoreType))\n",
    "stores_dict['Assortment'] = dict(zip(stores.Store, stores.Assortment))\n",
    "stores_dict['CompetitionDistance'] = dict(zip(stores.Store, stores.CompetitionDistance))\n",
    "stores_dict['CompetitionOpenSinceMonth'] = dict(zip(stores.Store, stores.CompetitionOpenSinceMonth))\n",
    "stores_dict['CompetitionOpenSinceYear'] = dict(zip(stores.Store, stores.CompetitionOpenSinceYear))\n",
    "stores_dict['Promo2'] = dict(zip(stores.Store, stores.Promo2))\n",
    "stores_dict['Promo2SinceWeek'] = dict(zip(stores.Store, stores.Promo2SinceWeek))\n",
    "stores_dict['Promo2SinceYear'] = dict(zip(stores.Store, stores.Promo2SinceYear))\n",
    "stores_dict['PromoInterval'] = dict(zip(stores.Store, stores.PromoInterval))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in stores.columns[1:]:\n",
    "    train[col] = train['Store'].map(stores_dict[col])\n",
    "train.Date = train.Date.apply(lambda x: pd.Timestamp(x))\n",
    "train = train.sort_values(['Date'])\n",
    "                           \n",
    "for col in stores.columns[1:]:\n",
    "    test[col] = test['Store'].map(stores_dict[col])\n",
    "test.Date = test.Date.apply(lambda x: pd.Timestamp(x))\n",
    "\n",
    "# Также нужно не забыть перевести StateHoliday в значения int. \n",
    "# Соображения те же, что и с признаками StoreType и Assortment\n",
    "train['StateHoliday'] = train['StateHoliday'].map({'0': 0, 0: 0, 'a': 1, 'b': 2, 'c': 3})\n",
    "test['StateHoliday'] = test['StateHoliday'].map({'0': 0, 0: 0, 'a': 1, 'b': 2, 'c': 3})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Удобнее будет, если целевой признак находится в конце таблицы.   \n",
    "В дальнейшем будем работать только с этой таблицей."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = train['Sales']\n",
    "train = train.drop('Sales', axis=1)\n",
    "train['Sales'] = y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Изучим пропуски и нулевые значения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cnt_nans = pd.DataFrame()\n",
    "cnt_nans['NansCount'] = pd.Series(np.empty(train.shape[1]))\n",
    "cnt_nans['NansPersentage'] = pd.Series(np.empty(train.shape[1]))\n",
    "cnt_nans.index = train.columns\n",
    "for col in train.columns:\n",
    "    cnt_nans['NansCount'][col] = np.sum(train[col].isna())\n",
    "    cnt_nans['NansPersentage'][col] = np.sum(train[col].isna()) / train.shape[0]\n",
    "cnt_nans['NansCount'] = cnt_nans['NansCount'].astype('int')\n",
    "cnt_nans"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cnt_nans = pd.DataFrame()\n",
    "cnt_nans['NansCount'] = pd.Series(np.empty(test.shape[1]))\n",
    "cnt_nans['NansPersentage'] = pd.Series(np.empty(test.shape[1]))\n",
    "cnt_nans.index = test.columns\n",
    "for col in test.columns:\n",
    "    cnt_nans['NansCount'][col] = np.sum(test[col].isna())\n",
    "    cnt_nans['NansPersentage'][col] = np.sum(test[col].isna()) / test.shape[0]\n",
    "cnt_nans['NansCount'] = cnt_nans['NansCount'].astype('int')\n",
    "cnt_nans"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно присутствует немало пропусков в данных, но они соответствуют их природе. То есть, например, нет Promo2 значит нет и даты начала Promo2.\n",
    "\n",
    "Признак Open имеет 11 значений NaN в тестовом датасете."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cnt_zeros = pd.DataFrame()\n",
    "cnt_zeros['ZerosCount'] = pd.Series(np.empty(train.shape[1]))\n",
    "cnt_zeros['ZerosPersentage'] = pd.Series(np.empty(train.shape[1]))\n",
    "cnt_zeros.index = train.columns\n",
    "for col in train.columns:\n",
    "    if (train[col].dtype != 'int'):\n",
    "        continue\n",
    "    cnt_zeros['ZerosCount'][col] = np.sum(train[col] == 0)\n",
    "    cnt_zeros['ZerosPersentage'][col] = np.sum(train[col] == 0) / train.shape[0]\n",
    "cnt_zeros['ZerosCount'] = cnt_zeros['ZerosCount'].astype('int')\n",
    "cnt_zeros"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данных есть очень много нулей, но нужно вспомнить, что тип _a_ заменялся на 0, а также есть бинарные признаки, значения 0 для которых вполне естественны. Более интересны нулевые значения целевой переменной и признаков Customers и Open. Они почти совпадают и отражают логику происходящего: если магазин закрыт, значит нет и клиентов, и оброта.   "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Проверим это\n",
    "train['Sales'][(train['Open'] == 0)].max(), train['Sales'][(train['Open'] == 0)].shape[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Но откуда остальные нули? Посмотрим позже."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Open имеет тип float. Заменим NaN на 1. Из того, что выше следует, что из тренировочного датасета можно выкинуть все записи с закрытыми магазинами. Если мы заменим NaN на 0, то можем и не попасть в правильное значение, а заменив на 1 предскажем какое-то значение ближе к правде."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test['Open'].fillna(1, inplace=True)\n",
    "test['Open'] = test['Open'].astype('int')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cnt_zeros = pd.DataFrame()\n",
    "cnt_zeros['ZerosCount'] = pd.Series(np.empty(test.shape[1]))\n",
    "cnt_zeros['ZerosPersentage'] = pd.Series(np.empty(test.shape[1]))\n",
    "cnt_zeros.index = test.columns\n",
    "for col in test.columns:\n",
    "    if (test[col].dtype != 'int'):\n",
    "        continue\n",
    "    cnt_zeros['ZerosCount'][col] = np.sum(test[col] == 0)\n",
    "    cnt_zeros['ZerosPersentage'][col] = np.sum(test[col] == 0) / test.shape[0]\n",
    "cnt_zeros['ZerosCount'] = cnt_zeros['ZerosCount'].astype('int')\n",
    "cnt_zeros"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ничем от тренировочного датасета не отличается."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "describe не сильно помогает, но можно бегом взглянуть на средние значения и стандартные отклонения. Остальное лучше на графиках смотреть."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train.describe()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на гистограммы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train.hist(figsize=(15, 15));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test.hist(figsize=(15, 15));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Многие из этих графиков уже были нарисованы при рассмотре таблицы store. Только сейчас изображено распределение числа наблюдений. Это, однако, не сильно меняет картину. Какие-то немного сгладились, а какие-то немного выделились.\n",
    "\n",
    "Есть только один интересный момент. В тесте нет наблюдений во время Рождества и Пасхи"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(8, 6))\n",
    "sns.distplot(train['Sales']);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "normaltest(train['Sales'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "skew(train['Sales'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "skewtest(train['Sales'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "normaltest отвергает гипотезу о нормальности распределения. Тест на скошенность говорит, что правый хвост имеет больший вес, что впрочем и видно на графике. Это может ухудшить качество линейной модели. Поэтому попробуем прологарифмировать. Также уберем нулевые значения, так как понятно откуда они появляются."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(8, 6))\n",
    "sns.distplot(np.log1p(train['Sales'][(train['Sales'] != 0)]));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "normaltest(np.log1p(train['Sales'][(train['Sales'] != 0)]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "skew(np.log1p(np.log1p(train['Sales'][(train['Sales'] != 0)])))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "skewtest(np.log1p(train['Sales'][(train['Sales'] != 0)]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Явно стало лучше."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(train['StateHoliday'], train['SchoolHoliday'], margins=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По первой строчке становится ясно, что SchoolHoliday подразумевает еще и каникулы.  \n",
    "Во время рождества все наблюдения подвергались действию школьных выходных(каникул). Видимо это особенность собранных данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(test['StateHoliday'], test['SchoolHoliday'], margins=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В трейне выходных первого типа и одновременно оказавших вляние школьных выходных в три раза меньше чем таких же выходных, не оказывавших влияние, в тесте их вообще нет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Следует взглянуть на корреляции числовых признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "corr_cols = ['Customers', 'CompetitionDistance', 'Sales']\n",
    "sns.heatmap(train[corr_cols].dropna().corr(), annot=True, fmt = \".2f\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Кратко, что привлекает внимание"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* Promo2SinceWeek имеет два больших периода с относительно высоким числом наблюдений.\n",
    "* Примерно половина магазинов имеют стандартный формат или стандартный ассортимент. Магазинов между расширенным ассортиментом и стандартным очень мало.\n",
    "* В датасете много пропусков, но большинство из них логичные.\n",
    "* Целевой признак очень сильно коррелирует с признаком Customers, что естественно. Только это зачение при предсказании мы знать не будем.\n",
    "* Явно видна связь между закрытостью магазина и числом клиентов, объемом продаж.\n",
    "* При использовании линейной модели таргет стоит прологарифмировать."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 3. Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_grouped_bar(features, figsize=None, train=train):\n",
    "    \"\"\"Строит средние таргета по какому-то, чаще категориальному, признаку в виде столбцов.\n",
    "    \n",
    "    Parameters\n",
    "    ----------\n",
    "    features: list string'ов\n",
    "        Названия признаков для визуализации.\n",
    "    figsize: tuple, 2 числа\n",
    "        Размеры графиков.\n",
    "    train: pd.DataFrame, default global\n",
    "        Датасет для визуализации. Должны присутствовать признаки features.\n",
    "    \"\"\"\n",
    "    _, axes = plt.subplots(len(features) // 3 + (len(features) % 3 > 0), min(3, len(features)), \n",
    "                           sharey=True, figsize=figsize)\n",
    "    \n",
    "    try:\n",
    "        axes = axes.reshape(-1)\n",
    "    except:\n",
    "        axes = [axes]\n",
    "    \n",
    "    for ind, feature in enumerate(features):\n",
    "        gr = train.groupby(feature)\n",
    "        xx = gr.groups.keys()\n",
    "        yy = gr['Sales'].mean()\n",
    "\n",
    "        df = pd.DataFrame()\n",
    "        df['SalesMean'] = yy\n",
    "        df[feature] = xx\n",
    "\n",
    "        sns.barplot(feature, 'SalesMean', data=df, ax=axes[ind])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Численные и временные признаки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так как была выявлена высокая корреляция между целевым признаком и признаком Customers, посмотрим на график их взаимного распределения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.jointplot(x='Sales', y='Customers', data=train);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Прослеживается линейная зависимость, что впрочем соответствует природе данных. Также видно, что разброс относительно среднего значения большой, но у него странная форма: сначала как и положено разброс постепенно увеличивается, а потом достаточно резко уменьшается."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, axes = plt.subplots(1, 2, figsize=(15, 7))\n",
    "sns.boxplot(x='DayOfWeek', y='Customers', data=train, ax=axes[0]);\n",
    "sns.boxplot(x='DayOfWeek', y='Sales', data=train, ax=axes[1]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Вот и еще одно полезное наблюдение. В последний день клиентов нет. В выходные магазины не работают. Не то, чтобы совсем, но по крайне мере большинство.\n",
    "\n",
    "Вот и узнаем от чего это зависит."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['HolidayAndType'] = train['StateHoliday'].apply(lambda x: str(x)) + '_' + \\\n",
    "    train['StoreType'].apply(lambda x: str(x))\n",
    "get_grouped_bar(['HolidayAndType'], (10, 5))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.sum(train[train['StateHoliday'].isin([2, 3]) & (train['StoreType'] != 2)]['Sales'] > 0) # Проверим увиденное\n",
    "# Масштаб обманывает"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train.drop('HolidayAndType', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Очень интересный график! Видно, что во время Рождесства и Пасхи все магазины кроме второго типа не работают совсем(хотелось бы так положить, но, как видим, это не совсем верно). А также в этих магазинах есть что-то особенное, что они не только во время праздников имеют высокий оборот, но и в рабочее время тоже. Интересно, а они вообще могут не работать? (Разумеется по причине отличной от Open == 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train[(train['Sales'] == 0) & (train['StoreType'] == 2) & (train['StateHoliday'] == 3) & (train['Open'] == 1)]['Store'].shape[0],\\\n",
    "train[(train['Sales'] == 0) & (train['StoreType'] == 2) & (train['StateHoliday'] == 2) & (train['Open'] == 1)]['Store'].shape[0],\\\n",
    "train[(train['Sales'] == 0) & (train['StoreType'] == 2) & (train['StateHoliday'] == 1) & (train['Open'] == 1)]['Store'].shape[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train[(train['Sales'] == 0) & (train['StoreType'] == 2) & (train['Open'] == 1) & (train['StateHoliday'] == 0)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видимо, нет, а три наблюдения выше скорее всего выбросы."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заменим пропущенные значения CompetitionDistance достаточно большими числами, чтобы увидеть, как влияет на оборот отсутствие конкурентов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.jointplot(x=\"Sales\", y=\"CompetitionDistance\", data=train.fillna(100000));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Магазины с отсутствующей конкуренцией или, что почти одно и то же, с конкурентами расположенными далеко имеют меньший оброт, а в тех случаях, когда конкурирующие магазины расположены близко, оборот выше. Это может показаться странным, но не стоит путать оборот с прибыльностью. Просится мысль, что там, где большее число посетителей, больше оборот, и поэтому конкуренты тоже стараются открывать в таких местах свои магазины.  \n",
    "Также график немного похож на график дискретной величины. Возможно из-за того, что люди склонны к округлениям."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Создадим для визуализации вспомогательные признаки, связанные со датой наблюдения, которые также могут пригодиться в будущем, и изучим их вляние на целевую переменную."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "day = train['Date'].apply(lambda x: x.day)\n",
    "month = train['Date'].apply(lambda x: x.month)\n",
    "year = train['Date'].apply(lambda x: x.year)\n",
    "year_mnth = year * 100 + month\n",
    "mnths = year * 12 + month - 2013*12\n",
    "train['Year'] = year\n",
    "train['MonthsFromStart'] = mnths\n",
    "train['YearMonth'] = month\n",
    "train['MonthDay'] = day\n",
    "train['DaysFromStart'] = (year * 365.5 + month*30.4 + day - 2013 * 365.5).astype('int')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "day = test['Date'].apply(lambda x: x.day)\n",
    "month = test['Date'].apply(lambda x: x.month)\n",
    "year = test['Date'].apply(lambda x: x.year)\n",
    "year_mnth = year * 100 + month\n",
    "mnths = year * 12 + month - 2013*12\n",
    "test['Year'] = year\n",
    "test['MonthsFromStart'] = mnths\n",
    "test['YearMonth'] = month\n",
    "test['MonthDay'] = day\n",
    "test['DaysFromStart'] = (year * 365.5 + month*30.4 + day - 2013 * 365.5).astype('int')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построим графики средних занчений таргета для каждого:\n",
    "1. Года\n",
    "2. Месяца с начала наблюдений\n",
    "3. Месяца в году\n",
    "4. Дня в месяце\n",
    "5. Дня с начала наблюдений"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mask_no_zeros = train['Open'] != 0 # Мы знаем, что их можно выбросить. Это сделает графики более гладкими\n",
    "gr = train[mask_no_zeros].groupby('Year')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(xx, yy);\n",
    "plt.xlabel(\"Year\");\n",
    "plt.ylabel(\"Sales Mean\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train[mask_no_zeros].groupby('MonthsFromStart')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(xx, yy);\n",
    "plt.xlabel(\"Months from the Start\");\n",
    "plt.ylabel(\"Sales Mean\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Есть два очень высоких значения. Они соответствуют концу года. А что бывает в конце года?  \n",
    "Правильно, Рождество."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train.groupby('YearMonth')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(xx, yy);\n",
    "plt.xlabel(\"Month of the Year\");\n",
    "plt.ylabel(\"Sales Mean\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Кроме этого есть еще и другие интересные месяцы: третий и седьмой."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train[mask_no_zeros].groupby('MonthDay')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(10, 5))\n",
    "plt.plot(xx, yy);\n",
    "plt.xlabel(\"Day of the Month\");\n",
    "plt.ylabel(\"Sales Mean\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В 31ый день начинается падение и оно продолжается до 11 числа следующего месяца. Дальше среднее значение оборота возрастает до примерно 17 числа и падает до 24го. После 24го растет до 30го."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train[mask_no_zeros].groupby('DaysFromStart')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(20, 5))\n",
    "plt.plot(xx, yy);\n",
    "plt.xlabel(\"Days from the Start\");\n",
    "plt.ylabel(\"Sales Mean\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Добавление признаков отражающих найденные сезоннсти явно может помочь при построении модели."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Промоакции"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построем график зависимости среднего значения целевой перменной от времени начала акции Promo2."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train.groupby('Promo2SinceWeek')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(15, 5))\n",
    "plt.plot(xx, yy);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train.groupby('Promo2SinceYear')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(15, 5))\n",
    "plt.plot(xx, yy);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что падение среднего значения иногда совпадает с отсутствием стартов Promo2, но пока что сложно установить между этими переменными какую-то связь."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем визуализировать PromoInterval."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train.groupby('PromoInterval')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "df = pd.DataFrame()\n",
    "df['Mean'] = yy\n",
    "df['PromoInterval'] = xx\n",
    "\n",
    "sns.barplot('PromoInterval', 'Mean', data=df);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Пока что ничего сказать нельзя, но все равно этот признак нужно будет учесть при предобработке. Можно, например, OHE сделать по каждому месяцу."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "get_grouped_bar(['Promo'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "ВрЕменные промоакции явно помогают повысить оборот."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Снова о конкуренции"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train.groupby('CompetitionOpenSinceYear')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(15, 5))\n",
    "plt.plot(xx, yy);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['TimeFromCompetitionBegin'] = train['CompetitionOpenSinceYear'] - train['Year']\n",
    "test['TimeFromCompetitionBegin'] = test['CompetitionOpenSinceYear'] - test['Year']\n",
    "\n",
    "gr = train.groupby('TimeFromCompetitionBegin')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "\n",
    "plt.figure(figsize=(15, 5))\n",
    "plt.plot(xx, yy);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Кроме каких-то странных случаев, когда конкурент существует с начала или середины прошлого века(прямость отрезков ломаной в этих участках говорит о единичности таких случаев), наличие достаточно новых, но успевших приспособиться, конкурентов приводит к очевидному снижению прибыли. Запомним отрезок [-12; -3]. Это часть графика, где оборот ниже. Конкуренты открывшиеся после нас также понижают оборот, хотя стоит вспомнить агрессивный маркетинг ново-открывшихся магазинов и все становится понятно.  \n",
    "Графики года и времени прошедшего с появления конкурентов почти совпадают. Обусловлено это тем, что данные в выборке представлены в промежутке от 2013 до 2015 года.   \n",
    "Можно заметить, что очень малая доля графика расположена правее нуля. Это стоит изучить отдельно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.sum(train['TimeFromCompetitionBegin'] > 0), np.sum(test['TimeFromCompetitionBegin'] > 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В сравнении со всей выборкой наблюдений, где конкурент открылся позже нас, действительно не много. Стоит также отметить то, что мы не знаем, как определялось данное значение. Ведь конкурентов и несколько бывает."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['ThereIsCompetition'] = (~train['CompetitionDistance'].isna()).astype('int')\n",
    "test['ThereIsCompetition'] = (~test['CompetitionDistance'].isna()).astype('int')\n",
    "get_grouped_bar(['ThereIsCompetition'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Очередное потверждение вреда наличия конкрентов объему оборота."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Если посмотреть на зависимость от расстояния на котором расположен конкурент, тоже можно увидеть тренд."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gr = train.groupby('CompetitionDistance')\n",
    "xx = gr.groups.keys()\n",
    "yy = gr['Sales'].mean()\n",
    "yy_rolling = yy.rolling(window=100).mean()\n",
    "\n",
    "plt.figure(figsize=(15, 5))\n",
    "plt.plot(xx, yy_rolling);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "non_nan = ~yy_rolling.isna()\n",
    "np.argmin(yy_rolling[non_nan])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Именно после этого значения начинается рост среднего, а до него идет падение."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выходные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "get_grouped_bar(['StateHoliday', 'SchoolHoliday'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В то время как первый график вполне ожидаем, второй график говорит, что если магазин подвергся влиянию школьных выходных(каникул), то оборот должен быть выше. В голову приходит только подозрение, что такое поведение спровоцировано каникулами. А сколько именно каникул в этих данных?\n",
    "\n",
    "Ответ на этот вопрос был выше при анализе данных. Их 166210. Стравнивая график ниже с графиком выше, становится понятно, что основное влияние оказывают именно каникулы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['SchoolVacations'] = ((train['StateHoliday'] == 0) & (train['SchoolHoliday'] == 1)).astype('int')\n",
    "test['SchoolVacations'] = ((test['StateHoliday'] == 0) & (test['SchoolHoliday'] == 1)).astype('int')\n",
    "get_grouped_bar(['SchoolVacations'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Заглядывая в будущее создадим и визуализируем признак \"IsWeekend\"(является ли день выходным)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['IsWeekend'] = (train['DayOfWeek'] > 5).astype('int')\n",
    "test['IsWeekend'] = (test['DayOfWeek'] > 5).astype('int')\n",
    "get_grouped_bar(['IsWeekend'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что он сильно влияет на срднее по таргету."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ассортимент и вид магазина."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "get_grouped_bar(['Assortment', 'StoreType'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Похоже, что ассортимент часто привязан к типу магазина."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Номер магазина\n",
    "Чуть не забыли про то, что в данных ограниченное число магазинов, и поэтому есть смысл посмотреть на статистику по магазинам. Видно, что некоторые магазины показывают бОльшие значения оборота."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.jointplot('Sales', 'Store', data=train);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 4. Закономерности, \"инсайты\", особенности данных\n",
    "Подытожим все, что было сказано выше."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Закономерности:\n",
    "* Одной из причин нулевого оборота является закрытость магазина(Признак Open).\n",
    "* Из Assortment == 2 следует StoreType == 2\n",
    "* Тип магазина 1 склонен иметь стандартный ассортимент, а тип магазина 3 склонен иметь расширенный ассортимент.\n",
    "* Оборот и число клиентов сильно коррелируют.\n",
    "* Но имеют большой разброс необычной формы. Стоит построить прямую и оценить разброс ошибки на различных ее интервалах.\n",
    "* Магазины типа 2 работают всегда. Оттого и оборот они показывают больший.\n",
    "* Остальные в большинстве своем отдыхают во время праздников.\n",
    "* В данных есть тренд и сезоннсть. Линейная модель может неплохо восстановить тренд, а также сезоннсть при наличии подходящих признаков.\n",
    "* Признак Promo равный 1 заметно повышает средний объем оборота.\n",
    "* Конкуренты понижают оборот. Особенно те, что открылись не сильно давно и не слишком рано.\n",
    "* Начиная с некоторой точки конкуренты поблизости понижают оборот. Виден восходящий тренд зависящий от растояния.\n",
    "* До этой точки, наоборот, оборот экстремально высокий. Дело возможно в людности этих мест.\n",
    "* Если смотреть на общую картину, выходные понижают объем оборота.\n",
    "* Каникулы повышают оборот.\n",
    "* Некоторые магазины показывают бОльшие объемы оборота."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Трансформации признаков\n",
    "* При использовании линейной модели таргет стоит прологарифмировать."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Пропуски\n",
    "Есть признаки с очень большим количеством пропусков:\n",
    "* Promo2Since*, PromoInterval - 508031 пропусков. Природа: если магазин не участвует в Promo2, то и времени начала не существует.\n",
    "* CompetitionDistance - 2642 пропусков. Природа: пропуск скорее всего значит отсутствие конкурентов.\n",
    "* CompetitionSince* - 323348 пропусков. Природа: возможно просто некачественно собрали данные, возможно просто эти данные были неизвестны."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выбор метрики и модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выбор метрики"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "desc = train[['Sales']].describe()\n",
    "desc['Sales'] = desc['Sales'].values.astype('int')\n",
    "desc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.sort(train['Sales'].values)[-10:]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отбросим нулевые значения оборота.  \n",
    "Большая часть наблюдений расположена рядом со средним значением, но есть и те записи, которые имеют очень высокие объемы оборота. Организаторы выбрали метрику Root Mean Square Percentage Error (RMSPE). Она допускает большие ошибки для объектов с большим абсолютным значением и сильнее штрафует за ошибки на объектах с меньшим абсолютным значением. Организаторы выбрали ее. Занчит именно она лучше всего подходила под задачи бизнеса.\n",
    "Следовательно, оптимальным вариантом будет RMSPE. Его нужно будет минимизировать.\n",
    "\n",
    "RMSPE можно записать так:\n",
    "$$\\sqrt{\\frac{1}{n} \\sum_{i=1}^{n} \\Bigg( \\frac{y_i - \\hat{y}_i}{y_i} \\Bigg )^2}$$, где $n$ - число объектов.\n",
    "\n",
    "Если cловами, то мы смотрим на сколько процентов ошибаемся, возводим все ошибки в квадрат и суммируем, как в MSE, но потом еще и корень берем."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Решается задача регрессии. Учитывая наличие тренда и плохую способность \"деревянных\" моделей экстраполировать выбор падает на **линейные модели**. Они обучаются гораздо быстрее, имеют меньше гиперпараметров и легко справляются с большим числом признаков. А именно, будем использовать **линейную регрессию**. Стоит попробовать обычную и с lasso, ridge регуляризацией. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 5. Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Заполним пропуски"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Первым делом выделим из датасета все наблюдения с закрытыми магазинами. Их предсказаниия мы знаем и строить по ним модель не имеет смысла. Метрику тоже будем считать без них, так как они все равно будут приносить в нее нулевой вклад."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "closed_mask = train['Open'] == 0\n",
    "closed_train = train[closed_mask]\n",
    "train = train[~closed_mask]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "closed_mask = test['Open'] == 0\n",
    "closed_test = test[closed_mask]\n",
    "zeros_test = test[closed_mask]\n",
    "test = test[~closed_mask]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Конкурент где-то очень далеко - это то же самое, что его нет. Это соответствует тенденции, замеченной на одном из графиков выше. Заменим NaN значениями чуть болше максимума."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['CompetitionDistance'].max(), test['CompetitionDistance'].max()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['CompetitionDistance'].fillna(75860. + 10000., inplace=True)\n",
    "test['CompetitionDistance'].fillna(75860. + 10000., inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для признаков CompetitionOpenSince* подойдет среднее значение. Нули не будут иметь смысла, так как пользуемся мы линейной регрессией, а среднее будет этаким нейтральным вариантом при прогнозе. Других разумных вариантов не видно.Так же для TimeFromCompetitionBegin."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['CompetitionOpenSinceMonth'].fillna(int(train['CompetitionOpenSinceMonth'].mean()), inplace=True)\n",
    "train['CompetitionOpenSinceYear'].fillna(int(train['CompetitionOpenSinceYear'].mean()), inplace=True)\n",
    "test['CompetitionOpenSinceMonth'].fillna(int(test['CompetitionOpenSinceMonth'].mean()), inplace=True)\n",
    "test['CompetitionOpenSinceYear'].fillna(int(test['CompetitionOpenSinceYear'].mean()), inplace=True)\n",
    "train['TimeFromCompetitionBegin'].fillna(int(train['TimeFromCompetitionBegin'].mean()), inplace=True)\n",
    "test['TimeFromCompetitionBegin'].fillna(int(test['TimeFromCompetitionBegin'].mean()), inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "То же проделаем с признаками Promo2Since*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train['Promo2SinceWeek'].fillna(int(train['Promo2SinceWeek'].mean()), inplace=True)\n",
    "train['Promo2SinceYear'].fillna(int(train['Promo2SinceYear'].mean()), inplace=True)\n",
    "test['Promo2SinceWeek'].fillna(int(test['Promo2SinceWeek'].mean()), inplace=True)\n",
    "test['Promo2SinceYear'].fillna(int(test['Promo2SinceYear'].mean()), inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выделим таргет и выбросим признак Customers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X, y = train.drop(['Sales', 'Customers'], axis=1), train['Sales']\n",
    "y_t = np.log1p(y)\n",
    "mean, std = y_t.mean(), y_t.std()\n",
    "y_t = (y_t - mean) / std\n",
    "del train"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cоздадим функции для вычисления метрик."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def rmspe(estimator, X, y):\n",
    "    y_true = y\n",
    "    y_pred = estimator.predict(X)\n",
    "    \n",
    "    m = ~(y_true == 0)\n",
    "    return (np.sum(((y_true[m] - y_pred[m]) / y_true[m])**2) / y_true[m].shape[0])**0.5\n",
    "\n",
    "def rmspe_log(estimator, X, y):\n",
    "    \"\"\"Возвращает rmspe score. Используется, если таргет прологарифмирован.\"\"\"\n",
    "    y_true = np.expm1(y * std + mean)\n",
    "    y_pred = np.expm1(estimator.predict(X) * std + mean)\n",
    "    \n",
    "    m = ~(y_true < 1e-4)\n",
    "    return (np.sum(((y_true[m] - y_pred[m]) / y_true[m])**2) / y_true[m].shape[0])**0.5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Зафиксируем деление выборки для кросс-валидации. Будем делить выборку на 4 части. Обучаться сначла на 1ой и предсказывать для второй. Потом обучаться на 1ой и 2ой, а предсказвать для 3ей и так далее, пока есть на чем предсказывать."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ts = TimeSeriesSplit(n_splits=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Создадим функцию для вычисления значения метрики на кросс-валидации. Использоваться будет Lasso регрессия. Обычная линейная регрессия не справляется, так как между признаками есть линейно зависимые. Их выбросим позже."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_score(X):\n",
    "    \"\"\"Проверяет точность Lasso регрессии на кросс-валидации предварительно отмасштабировав данные.\"\"\"\n",
    "    linreg_pipe = Pipeline([('scaler', StandardScaler()), ('linreg', Lasso(alpha=0.01))])\n",
    "    return cross_val_score(linreg_pipe, X[X.columns[(X.dtypes == 'int64') | (X.dtypes == 'float64')]], \n",
    "                          y_t, scoring=rmspe_log, cv = ts, n_jobs=-1).mean()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Признаки будем добавлять только если они приносят пользу."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cur_score = get_score(X)\n",
    "cur_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Способа, как визуализировать признак PromoInterval не нашлось, поэтому мы попробуем распарсить его, закодировать в виде 12 бинарных признаков каждый из которых значит свой месяц:\n",
    "1. Если NaN, то везде будут нули.\n",
    "2. Иначе единицы будут на тех месяцах, которые в этой строке встречались.\n",
    "\n",
    "После, при отборе признаков, посмотрим полезный он или нет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "month_to_num = {\n",
    "    'Jan': 0,\n",
    "    'Feb': 1,\n",
    "    'Mar': 2,\n",
    "    'Apr': 3,\n",
    "    'May': 4,\n",
    "    'Jun': 5,\n",
    "    'Jul': 6,\n",
    "    'Aug': 7,\n",
    "    'Sept': 8,\n",
    "    'Oct': 9,\n",
    "    'Nov': 10,\n",
    "    'Dec': 11\n",
    "}\n",
    "num_to_month = dict(zip(month_to_num.values(), month_to_num.keys()))\n",
    "X['PromoInterval'].unique()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "not_na = ~X['PromoInterval'].isna()\n",
    "temp = np.zeros((X[not_na].shape[0], 12), dtype='int')\n",
    "for i, value in enumerate(X['PromoInterval'][not_na].apply(lambda x: x.split(','))):\n",
    "    for mon in value:\n",
    "        mon_num = month_to_num[mon]\n",
    "        temp[i, mon_num] = 1\n",
    "        \n",
    "one_hot_months = np.zeros((X.shape[0], 12), dtype='int')\n",
    "one_hot_months[not_na] = temp \n",
    "del temp\n",
    "    \n",
    "for i in range(12):\n",
    "    mon = num_to_month[i]\n",
    "    X[\"Promo2Renew\" + mon] = one_hot_months[:, i]\n",
    "del one_hot_months"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score = get_score(X)\n",
    "new_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Польза есть."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "not_na = ~test['PromoInterval'].isna()\n",
    "temp = np.zeros((test[not_na].shape[0], 12), dtype='int')\n",
    "for i, value in enumerate(test['PromoInterval'][not_na].apply(lambda test: test.split(','))):\n",
    "    for mon in value:\n",
    "        mon_num = month_to_num[mon]\n",
    "        temp[i, mon_num] = 1\n",
    "        \n",
    "one_hot_months = np.zeros((test.shape[0], 12), dtype='int')\n",
    "one_hot_months[not_na] = temp \n",
    "del temp\n",
    "    \n",
    "for i in range(12):\n",
    "    mon = num_to_month[i]\n",
    "    test[\"Promo2Renew\" + mon] = one_hot_months[:, i]\n",
    "del one_hot_months"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Переведем в dummy вид признаки, которые линейной регрессией иначе никак не интерпретируемы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "to_ohe_feats = ['StateHoliday', 'StoreType', 'Assortment', 'PromoInterval', 'Store']\n",
    "useful_feats = []\n",
    "new_features = pd.DataFrame(index=X.index)\n",
    "cur_score = get_score(X)\n",
    "for feat in to_ohe_feats:\n",
    "    dummies = pd.get_dummies(X[feat], prefix=feat)\n",
    "    new_score = get_score(pd.concat([X, dummies], axis=1))\n",
    "    if (new_score < cur_score):\n",
    "        cur_score = new_score\n",
    "        X = pd.concat([X, dummies], axis=1)\n",
    "        print(feat, \"is useful!\")\n",
    "        useful_feats.append(feat)\n",
    "del dummies\n",
    "X.drop(useful_feats, axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_features = pd.DataFrame(index=test.index)\n",
    "for feat in useful_feats:\n",
    "    new_features = pd.concat([new_features, pd.get_dummies(test[feat], prefix=feat)], axis=1)\n",
    "test = pd.concat([test, new_features], axis=1)\n",
    "del new_features\n",
    "test.drop(useful_feats, axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь от OHE пользы не нашлось.  \n",
    "Все не интовые и бесполезные в OHE виде признаки нужно выкинуть. Из даты тоже мы во время визуализации вытащили все нужные для работы далее признаки(день, месяц, год), поэтому теперь ее можно выбросить."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = X[X.columns[(X.dtypes == 'int64') | (X.dtypes == 'float64')]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выделим таргет. Пользуемся линейной регрессией и поэтому прологарифмируем его. Нужно не забывать считать метрику с непрологарифмированным таргетом."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "## Часть 6. Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Очень много признаков были созданы вне этого раздела, но, все же есть, что добавить.  Признаки будем добавлять на основе наблюдений полученных выше.\n",
    "\n",
    "Для начала это будут признаки отражающие сезонность. Мы поделим временную шкалу на части, в каждой из которых будем отсчитвать время с начала. \n",
    "Например, с 4 по 15 число таргет растет. создадим признак принимающий значения 1...12 для дней 4-15 и 0 для остальных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X['MonthlyTrendPart1'] = X['MonthDay'].apply(lambda x: x + 1 if (x <= 11) else 1 if (x == 31) else 0)\n",
    "X['MonthlyTrendPart2'] = X['MonthDay'].apply(lambda x: x - 11 if (x > 11 and x<= 17) else 0)\n",
    "X['MonthlyTrendPart3'] = X['MonthDay'].apply(lambda x: x - 17 if (x > 17 and x <= 24) else 0)\n",
    "X['MonthlyTrendPart4'] = X['MonthDay'].apply(lambda x: x - 24 if (x > 24 and x <= 30) else 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score = get_score(X)\n",
    "new_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Добавляем."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test['MonthlyTrendPart1'] = test['MonthDay'].apply(lambda test: test + 1 if (test <= 11) else 1 if (test == 31) else 0)\n",
    "test['MonthlyTrendPart2'] = test['MonthDay'].apply(lambda test: test - 11 if (test > 11 and test<= 17) else 0)\n",
    "test['MonthlyTrendPart3'] = test['MonthDay'].apply(lambda test: test - 17 if (test > 17 and test <= 24) else 0)\n",
    "test['MonthlyTrendPart4'] = test['MonthDay'].apply(lambda test: test - 24 if (test > 24 and test <= 30) else 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В конце года оборот тоже растет. То же самое происходит в третий и седьмые месяцы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "good_months = [3, 7, 12]\n",
    "\n",
    "for m in good_months:\n",
    "    X['NearGoodMonth'+str(m)] = (X['YearMonth'].isin([m])).astype('int')\n",
    "    test['NearGoodMonth'+str(m)] = (test['YearMonth'].isin([m])).astype('int')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score = get_score(X)\n",
    "new_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Средние значения целевого признака были ниже на отрезке [-12; -3] значений признака _TimeFromCompetitionBegin_."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X['StrongCompetitorOld'] = X['TimeFromCompetitionBegin'].isin(range(-12, -2, 1)).astype('int')\n",
    "test['StrongCompetitorOld'] = test['TimeFromCompetitionBegin'].isin(range(-12, -2, 1)).astype('int')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score = get_score(X)\n",
    "new_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тренд у признака _CompetitionDistance_ разный до значения 3510 и после. Поэтому создадим признак, позволяющий модели понять, на каком промежутке этого признака расположилось данное наблюдение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X['AfterDistancePoint'] = (X['CompetitionDistance'] <= 3510).astype('int')\n",
    "test['AfterDistancePoint'] = (test['CompetitionDistance'] <= 3510).astype('int')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score = get_score(X)\n",
    "new_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Помнится второй магазин показывал более высокие средние значения таргета."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X['Is2ndType'] = (X['StoreType'] == 2).astype('int')\n",
    "test['Is2ndType'] = (test['StoreType'] == 2).astype('int')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_score = get_score(X)\n",
    "new_score"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Отбор признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Будем начиная с признака Year по-одному добавлять признаки в новую таблицу, если они приносят пользу(улучшают скор). Хотя это не гарантирует наилучший скор, такая эвристика имеет право на существование. Таким образом мы очень сильно уменьшим признаковое пространство."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "get_score(X)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "new_X = pd.DataFrame(index=X.index)\n",
    "new_X['Year'] = X['Year']\n",
    "cur_score = get_score(new_X)\n",
    "for feat in X.columns[(X.columns != 'Year')]:\n",
    "    new_X[feat] = X[feat]\n",
    "    new_score = get_score(new_X)\n",
    "    if (new_score < cur_score):\n",
    "        cur_score = new_score\n",
    "        print(feat, 'is useful!')\n",
    "    else:\n",
    "        new_X.drop(feat, axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = new_X\n",
    "cur_score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test = test[X.columns]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Процесс определенно имеет большую пользу!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 7. Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поделим датасет на отложенную и тренировочную части. Важно: нельзя указывать _shuffle=True_, чтобы не было лика и валидация была корректной."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_valid, y_train, y_valid = train_test_split(X.values, y_t.values, test_size=0.3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим обычную линейную регрессию.\n",
    "Подберем на кросс-валидации параметры для _Lasso_ и _Ridge_ регрессий с помощью _LassoCV_ и _RidgeCV_. Подбирать параметры будем на данных X_train. Деление для кросс-валидации мы уже определили заранее, когда признаки добавляли."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Прежний лучший результат\n",
    "Это Lasso регрессия с параметром _alpha=0.01_."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lasso = Lasso(alpha=0.01)\n",
    "lasso.fit(X_train, y_train)\n",
    "rmspe_log(lasso, X_valid, y_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Линейная регрессия\n",
    "Подбирать тут нечего. Обучим и проверим точность на кроссвалидации. Для этого определим такую же фнкцию, какой до этого пользовались для Lasso регрессии. Запомним значение метрики."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_score(X, y):\n",
    "    \"\"\"Проверяет точность линейной регрессии на кросс-валидации предварительно отмасштабировав данные.\"\"\"\n",
    "    linreg_pipe = Pipeline([('scaler', StandardScaler()), ('linreg', LinearRegression())])\n",
    "    return cross_val_score(linreg_pipe, X, \n",
    "                          y, scoring=rmspe_log, cv = ts, n_jobs=-1).mean()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "get_score(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Lasso\n",
    "Проведем кросс-валидацию. Параметр alpha будм подбирать на стандартном отрезке и 100 итерациях. В LassoCV нам не предоставлена возможность самим выбрать метрику, поэтому пользоваться будем стандартной. Не совсем корректно, учитвая также, что мы прологаримировали таргет, но вдруг это даст хороший скор в результате и для RMSPE. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь и далее используется масштабирование до подачи данных для кросс-валидации. Это не совсем корректно(среднее и отклонение утекает в тест), но на практике результаты мало отличаются."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lassoCV = LassoCV(cv=ts, n_jobs=-1, random_state=42, normalize=True) \n",
    "scaler = StandardScaler()\n",
    "lassoCV.fit(scaler.fit_transform(X_train), y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lassoCV.alpha_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rmspe_log(lassoCV, scaler.transform(X_valid), y_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Действительно лучше, чем раньше."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ridge"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ridgeCV = RidgeCV(cv=ts, scoring=rmspe_log)\n",
    "ridgeCV.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ridgeCV.alpha_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rmspe_log(ridgeCV, X_valid, y_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из-за масштаба признаков коэффицент регуляризации получился маленьким."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ridgeCV = RidgeCV(cv=ts, scoring=rmspe_log)\n",
    "scaler = StandardScaler()\n",
    "ridgeCV.fit(scaler.fit_transform(X_train), y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ridgeCV.alpha_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rmspe_log(ridgeCV, scaler.transform(X_valid), y_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тот же результат, но коэффицент другой. Принципиального улучшения в сравнении с Lasso не заметно, зато обе модели показали себя лучше, чем обычная линейная регрессия."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 8. Построение кривых валидации и обучения"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Валидационная кривая"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Воспользуемся функцие для построения валидационных кривых из sklearn. Она проводит кросс-валидацию с каждым параметром и возвращает полученные значения метрики."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lasso_pipe = Pipeline([('scaler', StandardScaler()), ('lasso', Lasso())])\n",
    "alphas = np.linspace(1e-3, 1., 100)\n",
    "val_train, val_test = validation_curve(lasso_pipe, X, y_t, param_name='lasso__alpha', \n",
    "                                       param_range=alphas, scoring=rmspe_log, cv=ts, n_jobs=-1) # не забываем, что таргет это y_t"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_error(x, scores, label):\n",
    "    \"Рисует кривые значений метрики в зависимости от параметра x при наличии результатов кросс валидации.\"\n",
    "    mean_scores, std_scores = scores.mean(axis=1), scores.std(axis=1)\n",
    "    plt.plot(x, mean_scores, label=label)\n",
    "    plt.fill_between(x, mean_scores - std_scores, mean_scores + std_scores, alpha=0.2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 7))\n",
    "\n",
    "plot_with_error(alphas, val_train, label=\"Train score\")\n",
    "plot_with_error(alphas, val_test, label=\"Test score\")\n",
    "plt.xlabel('alpha')\n",
    "plt.ylabel('RMSPE')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Явно видно, что до какого-то момента значение метрики ухудшается, а потом выходит на плато. Меньшие значения параметра регуляризации дают лучший скор. Это говорит о недообучении.\n",
    "\n",
    "Очень странно то, что значения метрики на тесте всегда получаются ниже, но разброс сильно выше."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Кривая обучения"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построим кривую обучения с коэффицентном регуляризации подобранном на кросс-валидации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lasso_pipe = Pipeline([('scaler', StandardScaler()), ('lasso', Lasso(alpha=0.0005162441836497342))])\n",
    "train_sizes = np.linspace(0.3, 1, 100)\n",
    "num_train, val_train, val_test = learning_curve(lasso_pipe, X, y_t, \n",
    "                                                train_sizes=train_sizes, cv=ts, n_jobs=-1, scoring=rmspe_log)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 7))\n",
    "\n",
    "plot_with_error(train_sizes, val_train, label=\"Train score\")\n",
    "plot_with_error(train_sizes, val_test, label=\"Test score\")\n",
    "plt.xlabel('Train set size')\n",
    "plt.ylabel('RMSPE')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ситуация со значениями метрики на тесте очень похожа на валидационную кривую.   \n",
    "Есть резкий скачок скора на трейне. Это может означать, что в это время появились новые, не похожие на ранее увиденные данные. \n",
    "\n",
    "Видно, что скор на трейне еще падает и не сошелся с тестом и поэтому дополнительные данные помогут."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 9. Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучим Lasso(alpha=0.0005162441836497342) на всей выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lasso_pipe = Pipeline([('scaler', StandardScaler()), ('lasso', Lasso(alpha=0.0005162441836497342))])\n",
    "lasso_pipe.fit(X, y_t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def write_to_submission_file(filename, prediction):\n",
    "    \"\"\"Записает предсказания в файл, как в примере.\"\"\"\n",
    "    sample = pd.read_csv('data/sample_submission.csv', index_col='Id')\n",
    "    sample['Sales'] = prediction\n",
    "    sample.to_csv('data/'+filename)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions = np.expm1(lasso_pipe.predict(test) * std + mean)\n",
    "final_pred = np.zeros(closed_mask.shape[0])\n",
    "final_pred[~closed_mask] = predictions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "write_to_submission_file('lasso_submission.csv', final_pred)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!kaggle competitions submit -c rossmann-store-sales -f data/lasso_submission.csv -m \"lasso regression\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Private part составлен из 61% данных. Вот значения метрики на тестовом датасете.\n",
    "\n",
    "\n",
    "<img src='../../img/leaderboard.png'>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Часть 11. Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Было найдено очень много зависимостей в данных, как и между признаками, так и с целевой переменной. Многие из признаков разработанных на их основе оказалсь полезными. Данная модель вряд ли может принести пользу бизнесу, так как предсказания имеют большой разброс, чего наоборот хотелось избежать.  \n",
    "\n",
    "Несмотря на то, что основанно была выбрана линейная модель, оказалось, что она недостаточно мощная. Это исправить помогли некоторые из разработанных признаков. Стоит попробовать бустинг и нейронные сети, так как зависимость часто была нелинейной.\n",
    "Также можно было сначала предсказать отдельной моделью число клиентов, для чего были данные, а уже потом объем оборота. Также стоит рассмотреть подход, когда для каждого магазина предасказания делаются отдельно."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
